[Android][Framework] 无障碍快捷方式相关代码


问题:无障碍快捷方式(Accessibility Shortcut)打开不生效。

如图,打开功能后,长按power键会出现振动,震动后双指放在屏幕上会打开无障碍。

无障碍的功能从来没有接触过,也不清楚在哪个模块修改,所以下面记录一下如何快速定位这种问题的思路:

在Opengrok检索"Accessibility Shortcut"找到字串accessibility_global_gesture_preference_title,可以确定两个地方:

  • Settings里Accessibility选项的入口 packages/apps/Settings/res/xml/accessibility_settings.xml
  • Accessibility的控制代码 packages/apps/Settings/src/com/android/settings/accessibility/AccessibilitySettings.java
AccessibilitySettings.java

通过阅读方法列表,知道这个类完全是用来控制Settings的Accessibility界面按钮的逻辑。

从控制代码,知道打开上图界面的代码是:

private void handleToggleEnableAccessibilityGesturePreferenceClick() {
    Bundle extras = mGlobalGesturePreferenceScreen.getExtras();
    extras.putString(EXTRA_TITLE, getString(
        R.string.accessibility_global_gesture_preference_title));
    extras.putString(EXTRA_SUMMARY, getString(
        R.string.accessibility_global_gesture_preference_description));
    extras.putBoolean(EXTRA_CHECKED, Settings.Global.getInt(getContentResolver(),Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1);
    super.onPreferenceTreeClick(mGlobalGesturePreferenceScreen);
}

这一步是设置Title和Summary,和设置SwichBar是否check。所以这个KEY是控制变量的关键。

搜索Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED,可以了解到几个地方:

ToggleGlobalGesturePreferenceFragment.java

这里通过KEY的命名和相关类的命名,可以知道,Accessibility Shortcut打开后,相关的手势被称为Global Gesture,全局手势。了解命名也很重要,这对于分析不熟悉的模块很有帮助。

这个Fragment是处理SwichBar的回调。每当SwichBar状态变化,就会更新相关的值:

@Override
protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
    Settings.Global.putInt(getContentResolver(),
            Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, enabled ? 1 : 0);
}

前面两个类似乎并不是关键,后面的两个就不一样了。

PhoneWindowManager.java

这是个大类,涉及到非常多的控制逻辑,而且从命名就能知道它的核心功能。但实际上,对于这个问题,只需要看一部分逻辑:

Accessibility Shortcut功能是长按POWER键启用。在PWM中,有powerLongPress的处理逻辑:

private void powerLongPress() {
    final int behavior = getResolvedLongPressOnPowerBehavior();
    switch (behavior) {
        case LONG_PRESS_POWER_NOTHING:
            break;
        case LONG_PRESS_POWER_GLOBAL_ACTIONS:
            mPowerKeyHandled = true;
            if (!performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false)) {
                performAuditoryFeedbackForAccessibilityIfNeed();
            }
            showGlobalActionsInternal();
            break;
        case LONG_PRESS_POWER_SHUT_OFF:
        case LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM:
            mPowerKeyHandled = true;
            performHapticFeedbackLw(null, HapticFeedbackConstants.LONG_PRESS, false);
            sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
            mWindowManagerFuncs.shutdown(behavior == LONG_PRESS_POWER_SHUT_OFF);
            break;
    }
}

首先获取长按POWER键的行为:

private int getResolvedLongPressOnPowerBehavior() {
    if (FactoryTest.isLongPressOnPowerOffEnabled()) {
        return LONG_PRESS_POWER_SHUT_OFF_NO_CONFIRM;
    }
    return mLongPressOnPowerBehavior;
}

正常情况下都会直接返回mLongPressOnPowerBehavior,这个变量初始化如下:

mLongPressOnPowerBehavior = mContext.getResources().getInteger(
                com.android.internal.R.integer.config_longPressOnPowerBehavior);

这个值在frameworks/base/core/res/res/values/config.xml配置为1。所以在Switch会走到LONG_PRESS_POWER_GLOBAL_ACTIONS这个case。

在这个case下,首先会调用performHapticFeedbackLW方法,从名称看,是perform触觉反馈LW方法,最终目的是发出振动。

@Override
public boolean performHapticFeedbackLw(WindowState win, int effectId, boolean always) {
    if (!mVibrator.hasVibrator()) {
        return false;
    }
    final boolean hapticsDisabled = Settings.System.getIntForUser(mContext.getContentResolver(),
    Settings.System.HAPTIC_FEEDBACK_ENABLED, 0, UserHandle.USER_CURRENT) == 0;
    if (hapticsDisabled && !always) {
        return false;
    }
    long[] pattern = null;
    switch (effectId) {
        case HapticFeedbackConstants.LONG_PRESS:
            pattern = mLongPressVibePattern;
            break;
        case HapticFeedbackConstants.VIRTUAL_KEY:
            pattern = mVirtualKeyVibePattern;
            break;
        case HapticFeedbackConstants.KEYBOARD_TAP:
            pattern = mKeyboardTapVibePattern;
            break;
        case HapticFeedbackConstants.CLOCK_TICK:
            pattern = mClockTickVibePattern;
            break;
        case HapticFeedbackConstants.CALENDAR_DATE:
            pattern = mCalendarDateVibePattern;
            break;
        case HapticFeedbackConstants.SAFE_MODE_DISABLED:
            pattern = mSafeModeDisabledVibePattern;
            break;
        case HapticFeedbackConstants.SAFE_MODE_ENABLED:
            pattern = mSafeModeEnabledVibePattern;
            break;
        case HapticFeedbackConstants.CONTEXT_CLICK:
            pattern = mContextClickVibePattern;
            break;
        default:
            return false;
    }
    int owningUid;
    String owningPackage;
    if (win != null) {
        owningUid = win.getOwningUid();
        owningPackage = win.getOwningPackage();
    } else {
        owningUid = android.os.Process.myUid();
        owningPackage = mContext.getOpPackageName();
    }
    if (pattern.length == 1) {
        // One-shot vibration
        mVibrator.vibrate(owningUid, owningPackage, pattern[0], VIBRATION_ATTRIBUTES);
    } else {
        // Pattern vibration
        mVibrator.vibrate(owningUid, owningPackage, pattern, -1, VIBRATION_ATTRIBUTES);
    }
    return true;
}

如果上面返回为false,则没有振动,代码会进入performAuditoryFeedbackForAccessibilityIfNeed,即播放声音。

private void performAuditoryFeedbackForAccessibilityIfNeed() {
    if (!isGlobalAccessibilityGestureEnabled()) {
        return;
    }
    AudioManager audioManager = (AudioManager) mContext.getSystemService(
        Context.AUDIO_SERVICE);
    if (audioManager.isSilentMode()) {
        return;
    }
    Ringtone ringTone = RingtoneManager.getRingtone(mContext, Settings.System.DEFAULT_NOTIFICATION_URI);
    ringTone.setStreamType(AudioManager.STREAM_MUSIC);
    ringTone.play();
}

播放声音前的判断就是 通过获取标志位判断。

private boolean isGlobalAccessibilityGestureEnabled() {
    return Settings.Global.getInt(mContext.getContentResolver(),
        Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1;
}

最后调用的是showGlobalActionsInternal方法

void showGlobalActionsInternal() {
    sendCloseSystemWindows(SYSTEM_DIALOG_REASON_GLOBAL_ACTIONS);
    if (mGlobalActions == null) {
        mGlobalActions = new GlobalActions(mContext, mWindowManagerFuncs);
    }
    final boolean keyguardShowing = isKeyguardShowingAndNotOccluded();
    mGlobalActions.showDialog(keyguardShowing, isDeviceProvisioned());
    if (keyguardShowing) {
        // since it took two seconds of long press to bring this up,
        // poke the wake lock so they have some time to see the dialog.
        mPowerManager.userActivity(SystemClock.uptimeMillis(), false);
    }
}

所以这里看出,PWM只是处理了长按power键的逻辑。那长按之后,双指触摸屏幕的逻辑在哪控制呢?

GlobalActions.java

这个类蛮有意思的,通过类名知道他是全局的一个操作,里面出现大量的控制代码,而且很多和手势操作相关。有机会可以深入了解一下。

在onStart的地方,通过阅读注释,可以知道,这里处理打开Accessibility后的双指触摸,触摸时不销毁弹出的关机选项对话框。

代码中有一个非常重要的判断,它决定是否进入Accessibility模式。

@Override
protected void onStart() {
    // If global accessibility gesture can be performed, we will take care
    // of dismissing the dialog on touch outside. This is because the dialog
    // is dismissed on the first down while the global gesture is a long press
    // with two fingers anywhere on the screen.
    if (EnableAccessibilityController.canEnableAccessibilityViaGesture(mContext)) {
        mEnableAccessibilityController = new EnableAccessibilityController(mContext,
        new Runnable() {
            @Override
            public void run() {
                dismiss();
            }
        });
        super.setCanceledOnTouchOutside(false);
    } else {
        mEnableAccessibilityController = null;
        super.setCanceledOnTouchOutside(true);
    }

    super.onStart();
}
EnableAccessibilityController.java
public static boolean canEnableAccessibilityViaGesture(Context context) {
    AccessibilityManager accessibilityManager = AccessibilityManager.getInstance(context);
    // Accessibility is enabled and there is an enabled speaking
    // accessibility service, then we have nothing to do.
    if (accessibilityManager.isEnabled()
        && !accessibilityManager.getEnabledAccessibilityServiceList(
            AccessibilityServiceInfo.FEEDBACK_SPOKEN).isEmpty()) {
        return false;
    }
    // If the global gesture is enabled and there is a speaking service
    // installed we are good to go, otherwise there is nothing to do.
    return Settings.Global.getInt(context.getContentResolver(),
                                  Settings.Global.ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED, 0) == 1
        && !getInstalledSpeakingAccessibilityServices(context).isEmpty();
}

这里需要判断几层:

  • AccessibilityManager enable
  • AccessibilityManager 服务不为空。这里服务可以是自定义安装的。如果没有可用服务就无从打开。
  • ENABLE_ACCESSIBILITY_GLOBAL_GESTURE_ENABLED标志位打开,即前面界面的switch按钮打开。
  • talkbak必须安装。因为他需要语音播放一些内容,所以talkbak是必备的。

我遇到的问题就是手机没有集成GMS talkbak,导致Accessibility打开没反应。


文章作者: Wossoneri
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC 4.0 许可协议。转载请注明来源 Wossoneri !
评论
  目录